<?php
abstract class system_db_adapter_abstract {
	protected $_fetchMode = system_db::FETCH_ASSOC;
	
	/**
	 * Keys are UPPERCASE SQL datatypes or the constants
	 * Zend_Db::INT_TYPE, Zend_Db::BIGINT_TYPE, or Zend_Db::FLOAT_TYPE.
	 *
	 * Values are:
	 * 0 = 32-bit integer
	 * 1 = 64-bit integer
	 * 2 = float or decimal
	 *
	 * @var array Associative array of datatypes to values 0, 1, or 2.
	 */
	protected $_numericDataTypes = array(system_db::INT_TYPE => system_db::INT_TYPE, system_db::BIGINT_TYPE => system_db::BIGINT_TYPE, system_db::FLOAT_TYPE => system_db::FLOAT_TYPE);
	
	protected $_caseFolding = system_db::CASE_NATURAL;
	protected $_autoQuoteIdentifiers = true;
	
	protected $_allowSerialization = true; //允许对象序列化
	

	protected $_autoReconnectOnUnserialize = false;
	
	protected $_config = array();
	public function __construct(array $config) {
		$this->_checkRequiredOptions($config);
		
		//默认option
		$options = array(system_db::CASE_FOLDING => $this->_caseFolding, system_db::AUTO_QUOTE_IDENTIFIERS => $this->_autoQuoteIdentifiers);
		
		$driverOptions = array();
		//重定义options
		if(array_key_exists('options', $config)) {
			// can't use array_merge() because keys might be integers
			foreach((array) $config['options'] as $key => $value) {
				$options[$key] = $value;
			}
		}
		if(array_key_exists('driver_options', $config)&&!empty($config['driver_options'])) {
			// can't use array_merge() because keys might be integers
			foreach((array) $config['driver_options'] as $key => $value) {
				$driverOptions[$key] = $value;
			}
		}
		
		if(!array_key_exists('charset', $config)) {
			$config['charset'] = null;
		}
		
		if(!isset($config['persistent'])) {
			$config['persistent'] = false;
		}
		unset($config['type']);
		
		$this->_config = array_merge($this->_config, $config);
		$this->_config['options'] = $options;
		$this->_config['driver_options'] = $driverOptions;
		
		if(array_key_exists(system_db::CASE_FOLDING, $options)) {
			$case = (int) $options[system_db::CASE_FOLDING];
			switch($case) {
				case system_db::CASE_LOWER:
				case system_db::CASE_UPPER:
				case system_db::CASE_NATURAL:
					$this->_caseFolding = $case;
					break;
				default:
					throw new system_db_adapter_exception('Case must be one of the following constants: '.'system_db::CASE_NATURAL, system_db::CASE_LOWER, system_db::CASE_UPPER');
			}
		}
		// obtain quoting property if there is one
		if(array_key_exists(system_db::AUTO_QUOTE_IDENTIFIERS, $options)) {
			$this->_autoQuoteIdentifiers = (bool) $options[system_db::AUTO_QUOTE_IDENTIFIERS];
		}
		
		// obtain allow serialization property if there is one
		if(array_key_exists(system_db::ALLOW_SERIALIZATION, $options)) {
			$this->_allowSerialization = (bool) $options[system_db::ALLOW_SERIALIZATION];
		}
		
		// obtain auto reconnect on unserialize property if there is one
		if(array_key_exists(system_db::AUTO_RECONNECT_ON_UNSERIALIZE, $options)) {
			$this->_autoReconnectOnUnserialize = (bool) $options[system_db::AUTO_RECONNECT_ON_UNSERIALIZE];
		}
		unset($config, $options, $driverOptions);
		$profiler = false;
		if(array_key_exists(system_db::PROFILER, $this->_config)) {
			$profiler = $this->_config[system_db::PROFILER];
			unset($this->_config[system_db::PROFILER]);
		}
		$this->setProfiler($profiler);
	}
	//check the config must be set;
	protected function _checkRequiredOptions(array $config) {
		if(!array_key_exists('dbname', $config)) {
			throw new system_db_adapter_exception("Configuration array must have a key for 'dbname' that names the database instance");
		}
		
		if(!array_key_exists('username', $config)) {
			throw new system_db_adapter_exception("Configuration array must have a key for 'username' that names the database instance");
		}
		
		if(!array_key_exists('password', $config)) {
			throw new system_db_adapter_exception("Configuration array must have a key for 'password' that names the database instance");
		}
	
	}
	protected $_connection = null;
	public function getConnection() {
		$this->_connect();
		return $this->_connection;
	}
	
	protected $_defaultProfilerClass = 'system_db_profiler';
	protected $_profiler;
	public function setProfiler($profiler) {
		$enabled = null;
		$profilerClass = $this->_defaultProfilerClass;
		$profilerInstance = null;
		
		if(!!($profilerIsObject = is_object($profiler))) {
			if($profiler instanceof system_db_profiler) {
				$profilerInstance = $profiler;
			} else {
				throw new system_db_profiler_exception('Profiler argument must be an instance of either Zend_Db_Profiler'.' or Zend_Config when provided as an object');
			}
		}
		
		if(is_array($profiler)) {
			if(isset($profiler['enabled'])) {
				$enabled = (bool) $profiler['enabled'];
			}
			if(isset($profiler['class'])) {
				$profilerClass = $profiler['class'];
			}
			if(isset($profiler['instance'])) {
				$profilerInstance = $profiler['instance'];
			}
		} else 
			if(!$profilerIsObject) {
				$enabled = (bool) $profiler;
			}
		
		if($profilerInstance===null) {
			$profilerInstance = new $profilerClass();
		}
		
		if(!$profilerInstance instanceof system_db_profiler) {
			
			throw new system_db_profiler_exception('Class '.get_class($profilerInstance).' does not extend '.'system_db_profiler');
		}
		
		if(null!==$enabled) {
			$profilerInstance->setEnabled($enabled);
		}
		
		$this->_profiler = $profilerInstance;
		
		return $this;
	}
	
	protected $_defaultStmtClass = 'system_db_statement';
	
	/**
	 * 得到数据运行详情
	 * @return system_db_profiler
	 */
	public function getProfiler() {
		return $this->_profiler;
	}
	
	public function getStatementClass() {
		return $this->_defaultStmtClass;
	}
	
	public function setStatementClass($class) {
		$this->_defaultStmtClass = $class;
		return $this;
	}
	
	public function getConfig() {
		return $this->_config;
	}
	
	public function query($sql, $bind = array()) {
		// connect to the database if needed
		$this->_connect();
		
		if($sql instanceof system_db_select) {
			if(empty($bind)) {
				$bind = $sql->getBind();
			}
			
			$sql = $sql->assemble();
		}
		
		// make sure $bind to an array;
		// don't use (array) typecasting because
		// because $bind may be a Zend_Db_Expr object
		if(!is_array($bind)) {
			$bind = array($bind);
		}
		
		// prepare and execute the statement with profiling
		$stmt = $this->prepare($sql);
		$stmt->execute($bind);
		
		// return the results embedded in the prepared statement object
		$stmt->setFetchMode($this->_fetchMode);
		return $stmt;
	}
	
	public function beginTransaction() {
		$this->_connect();
		$q = $this->getProfiler()->queryStart('begin', system_db_profiler::TRANSACTION);
		$this->_beginTransaction();
		$this->getProfiler()->queryEnd($q);
		return $this;
	}
	
	public function commit() {
		$this->_connect();
		$q = $this->getProfiler()->queryStart('commit', system_db_profiler::TRANSACTION);
		$this->_commit();
		$this->_profiler->queryEnd($q);
		return $this;
	}
	
	public function rollBack() {
		$this->_connect();
		$q = $this->_profiler->queryStart('rollback', system_db_profiler::TRANSACTION);
		$this->_rollBack();
		$this->_profiler->queryEnd($q);
		return $this;
	}
	
	public function insert($table, array $bind) {
		// extract and quote col names from the array keys
		$cols = array();
		$vals = array();
		foreach($bind as $col => $val) {
			$cols[] = $this->quoteIdentifier($col, true);
			if($val instanceof system_db_expr) {
				$vals[] = $val->__toString();
				unset($bind[$col]);
			} else {
				$vals[] = '?';
			}
		}
		
		// build the statement
		$sql = "INSERT INTO ".$this->quoteIdentifier($table, true).' ('.implode(', ', $cols).') '.'VALUES ('.implode(', ', $vals).')';
		
		// execute the statement and return the number of affected rows
		$stmt = $this->query($sql, array_values($bind));
		$result = $stmt->rowCount();
		return $result;
	}
	
	public function update($table, array $bind, $where = '') {
		/**
		 * Build "col = ?" pairs for the statement,
		 * except for Zend_Db_Expr which is treated literally.
		 */
		$set = array();
		$i = 0;
		foreach($bind as $col => $val) {
			if($val instanceof system_db_expr) {
				$val = $val->__toString();
				unset($bind[$col]);
			} else {
				if($this->supportsParameters('positional')) {
					$val = '?';
				} else {
					if($this->supportsParameters('named')) {
						unset($bind[$col]);
						$bind[':'.$col.$i] = $val;
						$val = ':'.$col.$i;
						$i++;
					} else {
						throw new system_db_adapter_exception(get_class($this)." doesn't support positional or named binding");
					}
				}
			}
			$set[] = $this->quoteIdentifier($col, true).' = '.$val;
		}
		
		$where = $this->_whereExpr($where);
		
		/**
		 * Build the UPDATE statement
		 */
		$sql = "UPDATE ".$this->quoteIdentifier($table, true).' SET '.implode(', ', $set).(($where) ? " WHERE $where" : '');
		
		/**
		 * Execute the statement and return the number of affected rows
		 */
		if($this->supportsParameters('positional')) {
			$stmt = $this->query($sql, array_values($bind));
		} else {
			$stmt = $this->query($sql, $bind);
		}
		$result = $stmt->rowCount();
		return $result;
	}
	
	public function delete($table, $where = '') {
		$where = $this->_whereExpr($where);
		
		/**
		 * Build the DELETE statement
		 */
		$sql = "DELETE FROM ".$this->quoteIdentifier($table, true).(($where) ? " WHERE $where" : '');
		
		/**
		 * Execute the statement and return the number of affected rows
		 */
		$stmt = $this->query($sql);
		$result = $stmt->rowCount();
		return $result;
	}
	
	protected function _whereExpr($where) {
		if(empty($where)) {
			return $where;
		}
		if(!is_array($where)) {
			$where = array($where);
		}
		foreach($where as $cond => &$term) {
			// is $cond an int? (i.e. Not a condition)
			if(is_int($cond)) {
				// $term is the full condition
				if($term instanceof system_db_expr) {
					$term = $term->__toString();
				}
			} else {
				// $cond is the condition with placeholder,
				// and $term is quoted into the condition
				$term = $this->quoteInto($cond, $term);
			}
			$term = '('.$term.')';
		}
		
		$where = implode(' AND ', $where);
		return $where;
	}
	
	/**
	 * @return system_db_select
	 */
	public function select() {
		return new system_db_select($this);
	}
	
	public function getFetchMode() {
		return $this->_fetchMode;
	}
	
	public function fetchAll($sql, $bind = array(), $fetchMode = null) {
		if($fetchMode===null) {
			$fetchMode = $this->getFetchMode();
		}
		$stmt = $this->query($sql, $bind);
		$result = $stmt->fetchAll($fetchMode);
		return $result;
	}
	
	public function fetchRow($sql, $bind = array(), $fetchMode = null) {
		if($fetchMode===null) {
			$fetchMode = $this->getFetchMode();
		}
		$stmt = $this->query($sql, $bind);
		$result = $stmt->fetch($fetchMode);
		return $result;
	}
	
	public function fetchAssoc($sql, $bind = array()) {
		$stmt = $this->query($sql, $bind);
		$data = array();
		while(!!($row = $stmt->fetch(system_db::FETCH_ASSOC))) {
			$tmp = array_values(array_slice($row, 0, 1));
			$data[$tmp[0]] = $row;
		}
		return $data;
	}
	
	public function fetchCol($sql, $bind = array()) {
		$stmt = $this->query($sql, $bind);
		$result = $stmt->fetchAll(system_db::FETCH_COLUMN, 0);
		return $result;
	}
	public function fetchPairs($sql, $bind = array()) {
		$stmt = $this->query($sql, $bind);
		$data = array();
		while(!!($row = $stmt->fetch(system_db::FETCH_NUM))) {
			$data[$row[0]] = $row[1];
		}
		return $data;
	}
	
	public function fetchOne($sql, $bind = array()) {
		$stmt = $this->query($sql, $bind);
		$result = $stmt->fetchColumn(0);
		return $result;
	}
	
	protected function _quote($value) {
		if(is_int($value)) {
			return $value;
		} elseif(is_float($value)) {
			return sprintf('%F', $value);
		}
		return "'".addcslashes($value, "\000\n\r\\'\"\032")."'";
	}
	
	public function quote($value, $type = null) {
		$this->_connect();
		
		if($value instanceof system_db_select) {
			return '('.$value->assemble().')';
		}
		
		if($value instanceof system_db_expr) {
			return $value->__toString();
		}
		
		if(is_array($value)) {
			foreach($value as &$val) {
				$val = $this->quote($val, $type);
			}
			return implode(', ', $value);
		}
		
		if($type!==null&&array_key_exists($type = strtoupper($type), $this->_numericDataTypes)) {
			$quotedValue = '0';
			switch($this->_numericDataTypes[$type]) {
				case system_db::INT_TYPE: // 32-bit integer
					$quotedValue = (string) intval($value);
					break;
				case system_db::BIGINT_TYPE: // 64-bit integer
					// ANSI SQL-style hex literals (e.g. x'[\dA-F]+')
					// are not supported here, because these are string
					// literals, not numeric literals.
					if(preg_match('/^(
                          [+-]?                  # optional sign
                          (?:
                            0[Xx][\da-fA-F]+     # ODBC-style hexadecimal
                            |\d+                 # decimal or octal, or MySQL ZEROFILL decimal
                            (?:[eE][+-]?\d+)?    # optional exponent on decimals or octals
                          )
                        )/x', (string) $value, $matches)) {
						$quotedValue = $matches[1];
					}
					break;
				case system_db::FLOAT_TYPE: // float or decimal
					$quotedValue = sprintf('%F', $value);
			}
			return $quotedValue;
		}
		return $this->_quote($value);
	}
	public function quoteInto($text, $value, $type = null, $count = null) {
		if($count===null) {
			return str_replace('?', $this->quote($value, $type), $text);
		} else {
			while($count>0) {
				if(strpos($text, '?')!==false) {
					$text = substr_replace($text, $this->quote($value, $type), strpos($text, '?'), 1);
				}
				--$count;
			}
			return $text;
		}
	}
	
	public function quoteIdentifier($ident, $auto = false) {
		return $this->_quoteIdentifierAs($ident, null, $auto);
	}
	
	public function quoteColumnAs($ident, $alias, $auto = false) {
		return $this->_quoteIdentifierAs($ident, $alias, $auto);
	}
	
	public function quoteTableAs($ident, $alias = null, $auto = false) {
		return $this->_quoteIdentifierAs($ident, $alias, $auto);
	}
	
	protected function _quoteIdentifierAs($ident, $alias = null, $auto = false, $as = ' AS ') {
		if($ident instanceof system_db_expr) {
			$quoted = $ident->__toString();
		} elseif($ident instanceof system_db_select) {
			$quoted = '('.$ident->assemble().')';
		} else {
			if(is_string($ident)) {
				$ident = explode('.', $ident);
			}
			if(is_array($ident)) {
				$segments = array();
				foreach($ident as $segment) {
					if($segment instanceof system_db_expr) {
						$segments[] = $segment->__toString();
					} else {
						$segments[] = $this->_quoteIdentifier($segment, $auto);
					}
				}
				if($alias!==null&&end($ident)==$alias) {
					$alias = null;
				}
				$quoted = implode('.', $segments);
			} else {
				$quoted = $this->_quoteIdentifier($ident, $auto);
			}
		}
		if($alias!==null) {
			$quoted .= $as.$this->_quoteIdentifier($alias, $auto);
		}
		return $quoted;
	}
	
	protected function _quoteIdentifier($value, $auto = false) {
		if($auto===false||$this->_autoQuoteIdentifiers===true) {
			$q = $this->getQuoteIdentifierSymbol();
			return ($q.str_replace("$q", "$q$q", $value).$q);
		}
		return $value;
	}
	public function getQuoteIdentifierSymbol() {
		return '"';
	}
	
	public function lastSequenceId($sequenceName) {
		return null;
	}
	public function nextSequenceId($sequenceName) {
		return null;
	}
	public function foldCase($key) {
		switch($this->_caseFolding) {
			case system_db::CASE_LOWER:
				$value = strtolower((string) $key);
				break;
			case system_db::CASE_UPPER:
				$value = strtoupper((string) $key);
				break;
			case system_db::CASE_NATURAL:
			default:
				$value = (string) $key;
		}
		return $value;
	}
	
	//是否允许数据库对象的序列化
	public function __sleep() {
		if($this->_allowSerialization==false) {
			throw new system_db_adapter_exception(get_class($this)." is not allowed to be serialized");
		}
		$this->_connection = false;
		return array_keys(array_diff_key(get_object_vars($this), array('_connection' => false)));
	}
	public function __wakeup() {
		if($this->_autoReconnectOnUnserialize==true) {
			$this->getConnection();
		}
	}
	public function connection() {
		$this->_connect();
	}
	
	abstract public function listTables();
	abstract public function describeTable($tableName, $schemaName = null);
	abstract protected function _connect();
	abstract public function isConnected();
	abstract public function closeConnection();
	abstract public function prepare($sql);
	abstract public function lastInsertId($tableName = null, $primaryKey = null);
	abstract protected function _beginTransaction();
	abstract protected function _commit();
	abstract protected function _rollBack();
	abstract public function setFetchMode($mode);
	abstract public function limit($sql, $count, $offset = 0);
	
	/**
	 * Check if the adapter supports real SQL parameters.
	 *
	 * @param string $type 'positional' or 'named'
	 * @return bool
	 */
	abstract public function supportsParameters($type);
	
	abstract public function getServerVersion();

}